# evaluator.py
# -*- coding: utf-8 -*-
"""
单文件版中文眼底多任务评测器（含总体正确率与未知题型处理策略）
- 评测四类题型：judge / choice / compare / diagnosis
- 输入：含 prompt/label/predict/qtype（或 q_type）的 JSONL
- 输出：在每行记录中追加 score(0/0.5/1) 和 reason(中文可溯源说明)
- 词表可选：all_cn_disease.json（字符串列表或 {"names":[...]} 等）
- 新增：
  * strict_accuracy：仅 score==1 计为正确
  * weighted_accuracy：平均分（0.5 计 0.5）
  * --unknown_policy {strict,skip} 控制未知题型处理；skip 时仅在 -v 下告警
"""
from __future__ import annotations

import json
import argparse
import sys
import re
from typing import List, Dict, Tuple, Set, Optional


class ChineseMedicalEvaluator:
    """
    评测主类：提供 evaluate_file(in_jsonl, out_jsonl, unknown_policy)。
    """

    # —— 一些全局关键字与规则表 —— #
    POS_STRICT = ("是", "否")  # 严格判断
    POS_WEAK = ("可以", "能够", "可诊断为", "诊断为", "符合", "支持", "可见", "存在", "属于")
    NEG_WEAK = ("不是", "不可以", "不能", "不支持", "不符合", "不存在", "未见", "无明显", "无", "均不", "都不")

    BOTH_TRUE_HINTS_L = ("均", "同时", "二者", "两者", "都")
    BOTH_TRUE_HINTS_R = ("存在", "可见", "表现", "特征")

    BOTH_FALSE_HINTS_L = ("未见", "不支持", "均不", "都不", "无明显", "无")
    BOTH_FALSE_HINTS_R = ("诊断", "征象", "表现", "特征", "典型", "支持")

    CONNECTORS = ("或", "还是", "与", "和")

    NORMAL_SYNS = ("正常", "未见异常", "无异常", "无明显异常")

    CHOICE_ALLOWED = set(["A", "B", "C", "D"])

    PUNCT_MAP = {
        "，": ",", "、": ",", "：": ":", "；": ";", "。": "。", "！": "!", "？": "?",
        "（": "(", "）": ")", "【": "[", "】": "]", "“": "\"", "”": "\"", "‘": "'", "’": "'",
        "《": "<", "》": ">", "—": "-", "－": "-", "～": "~",
    }

    def __init__(self, disease_lexicon_path: Optional[str] = None, verbose: bool = False) -> None:
        self.verbose = verbose
        self.disease_lexicon: List[str] = []
        if disease_lexicon_path:
            self.disease_lexicon = self._load_lexicon(disease_lexicon_path)
        self.disease_lexicon.sort(key=len, reverse=True)  # 最长优先

        # 严格“是/否”边界匹配
        bd = r"(?:^|[\s,，。；;、:：!！?？])"
        self.strict_yes_re = re.compile(bd + r"是" + bd)
        self.strict_no_re = re.compile(bd + r"否" + bd)
        self.choice_letter_re = re.compile(r"[A-D]")

    # ========== 公共入口：文件级 ========== #
    def evaluate_file(self, in_jsonl: str, out_jsonl: str, metrics_json: Optional[str], unknown_policy: str) -> None:
        """
        对 JSONL 文件逐行打分并写回 JSONL；同时输出总体指标。
        unknown_policy:
          - 'strict'（默认）：未知题型直接报错
          - 'skip'：跳过未知题型/缺 qtype 的样本（仅在 verbose 下告警）

        指标说明：
          - strict_accuracy   : 仅 score == 1 视为正确
          - weighted_accuracy : 直接对 score 求平均（0 / 0.5 / 1 / 0.25 等）
          - loose_accuracy    : 只要 score > 0 都视为正确（宽松准确率）
        """
        total = 0               # 参与评测的样本数
        sum_scores = 0.0        # score 累计（用于 weighted_accuracy）
        strict_correct = 0      # score==1 的样本数
        loose_correct = 0       # score>0 的样本数（用于 loose_accuracy）
        skipped = 0             # 未识别题型/缺字段被跳过的样本数
        per_type = {}           # 每类计数与得分

        with open(in_jsonl, "r", encoding="utf-8") as fin, open(out_jsonl, "w", encoding="utf-8") as fout:
            for lineno, line in enumerate(fin, 1):
                line = line.strip()
                if not line:
                    continue
                try:
                    record = json.loads(line)
                except Exception as e:
                    self._warn(f"[Line {lineno}] 非法 JSON，跳过：{e}")
                    skipped += 1
                    continue

                prompt = record.get("prompt", "")
                label = record.get("label", "")
                predict = record.get("predict", "")
                qtype = record.get("qtype", record.get("q_type", None))

                # 未提供 qtype 或未知题型
                if not qtype or qtype not in {"judge", "choice", "compare", "diagnosis"}:
                    if unknown_policy == "skip":
                        skipped += 1
                        if self.verbose:
                            self._warn(f"[Line {lineno}] 跳过未知题型：{qtype!r}")
                        continue
                    else:
                        # 严格模式下，遇到未知题型直接报错
                        raise NotImplementedError(f"[Line {lineno}] 未识别的题型：{qtype!r}")

                # 规范化文本（内部判断用，不改输出）
                n_prompt = self._normalize(prompt)
                n_label = self._normalize(label)
                n_predict = self._normalize(predict)

                # 分发打分
                if qtype == "judge":
                    score, reason = self._score_judge(n_label, n_predict)
                elif qtype == "choice":
                    score, reason = self._score_choice(n_label, n_predict)
                elif qtype == "compare":
                    score, reason = self._score_compare(n_prompt, n_label, n_predict)
                else:  # diagnosis
                    score, reason = self._score_diagnosis(n_label, n_predict)

                # 写回一行
                out_obj = dict(record)
                out_obj["score"] = score
                out_obj["reason"] = reason
                fout.write(json.dumps(out_obj, ensure_ascii=False) + "\n")

                # 累计总体指标
                total += 1
                score_f = float(score)
                sum_scores += score_f
                if score_f == 1.0:
                    strict_correct += 1
                # loose_acc：只要分数大于 0 就视为宽松正确
                if score_f > 0.0:
                    loose_correct += 1

                # 分类型统计
                pt = per_type.setdefault(
                    qtype,
                    {"count": 0, "sum_scores": 0.0, "strict_correct": 0, "loose_correct": 0}
                )
                pt["count"] += 1
                pt["sum_scores"] += score_f
                if score_f == 1.0:
                    pt["strict_correct"] += 1
                if score_f > 0.0:
                    pt["loose_correct"] += 1

        # 汇总总体指标
        strict_accuracy = (strict_correct / total) if total > 0 else 0.0
        weighted_accuracy = (sum_scores / total) if total > 0 else 0.0
        loose_accuracy = (loose_correct / total) if total > 0 else 0.0

        # 组装 metrics
        metrics = {
            "evaluated": total,
            "skipped": skipped,
            "strict_accuracy": round(strict_accuracy, 6),
            "weighted_accuracy": round(weighted_accuracy, 6),
            "loose_accuracy": round(loose_accuracy, 6),
            "per_type": {
                k: {
                    "count": v["count"],
                    "strict_accuracy": round(
                        (v["strict_correct"] / v["count"]) if v["count"] else 0.0, 6
                    ),
                    "weighted_accuracy": round(
                        (v["sum_scores"] / v["count"]) if v["count"] else 0.0, 6
                    ),
                    "loose_accuracy": round(
                        (v["loose_correct"] / v["count"]) if v["count"] else 0.0, 6
                    ),
                }
                for k, v in per_type.items()
            },
        }

        # 写 metrics 文件（默认 out_jsonl + ".metrics.json"）
        metrics_path = metrics_json or (out_jsonl + ".metrics.json")
        with open(metrics_path, "w", encoding="utf-8") as mf:
            json.dump(metrics, mf, ensure_ascii=False, indent=2)

        # 控制台简报（不受 verbose 控制）
        print(
            f"[SUMMARY] evaluated={metrics['evaluated']}  skipped={metrics['skipped']}  "
            f"strict_acc={metrics['strict_accuracy']:.4f}  "
            f"weighted_acc={metrics['weighted_accuracy']:.4f}  "
            f"loose_acc={metrics['loose_accuracy']:.4f}  "
            f"metrics_file={metrics_path}"
        )

    # ========== judge ========== #
    def _score_judge(self, gold: str, pred: str) -> Tuple[float, str]:
        """
        判断题评分逻辑：
        - gold / pred 先通过 _detect_yes_no 解析为 yes/no/none/both
        - 严格“是/否”优先，其次再用弱同义词（可以/支持/存在/不支持/未见 等）做辅助判断
        - 额外规则（利用词表，参考 diagnosis 的思路）：
          问“是否 A”，若标准答案方向为“否”，模型未直接说“否”，
          而是给出了另一个疾病 B（B ≠ A），则视为语义正确。
        """
        gold_cls, gold_strict, gold_hits = self._detect_yes_no(gold)
        pred_cls, pred_strict, pred_hits = self._detect_yes_no(pred)

        # gold 同时包含“是/否”视为构造错误
        if gold_cls == "both":
            return 0.0, f"答案构造错误：同时包含‘是’与‘否’（命中：{gold_hits}）"

        # 模型输出同时包含“是/否”视为歧义
        if pred_cls == "both":
            return 0.0, f"模型输出歧义：同时包含‘是’与‘否’（命中：{pred_hits}）"

        # —— 情形 1：gold 本身没有直接解析出 yes/no，需要用弱同义词再尝试 —— #
        if gold_cls == "none":
            gold_weak = self._map_weak_yesno(gold)
            if gold_weak is None:
                # 标准答案既无严格“是/否”，也无可用弱同义词，无法判定
                return 0.0, "标准答案未提供可判定的‘是/否’（弱同义也未命中）"
            # 若 pred 与 gold 的弱同义方向一致 → 仅记 0.5 分（因为 gold 自身就不严格）
            if pred_cls == gold_weak:
                return 0.5, f"判为 {gold_weak}；但答案未用严格‘是/否’，记 0.5"
            else:
                return 0.0, f"gold弱同义={gold_weak}，pred={pred_cls}，不一致"

        # —— 情形 2：gold 已经有 yes/no，但 pred 完全解析不出方向 —— #
        if pred_cls == "none":
            return 0.0, "模型输出未解析出‘是/否’"

        # —— 情形 3：gold / pred 方向一致 —— #
        if gold_cls == pred_cls:
            # 双方都使用了严格“是/否”，给满分
            if gold_strict and pred_strict:
                return 1.0, "与标准一致，且双方使用严格‘是/否’"
            # 至少一方没用严格“是/否”，记 0.5
            else:
                return 0.5, "与标准一致，但至少一方未使用严格‘是/否’"

        # —— 情形 4：gold / pred 方向不一致 —— #
        # 这里插入一条“参考 diagnosis 的词表规则”：
        #   典型样例：
        #     prompt: “该眼底照片能诊断为重度糖尿病黄斑水肿吗？请回答是或否。”
        #     gold:   “否，未见重度糖尿病黄斑水肿的典型征象。”
        #     pred:   “图像中可见正常眼底的典型表现，诊断为正常眼底。”
        #   语义上是：问“是不是 A”，gold 明确是否定；模型虽然形式上没说“否”，
        #   但给出了“可见 B（正常眼底）”，而 B ≠ A，此时可视为“不是 A，而是 B”，
        #   语义正确，因此按“正确”记分。
        #
        # 具体实现：
        #   - 仅在 gold_cls == "no" 且 pred_cls == "yes" 时触发检查
        #   - 利用疾病词表，从 gold / pred 中抽取疾病集合：
        #       gold_diseases = 题目中被否定的目标疾病 A（及相关表达）
        #       pred_diseases = 模型给出的诊断 B
        #   - 若 pred_diseases 非空，且与 gold_diseases 不相交，
        #     则认为模型在回答“不是 A，而是 B” → 记为 0.25
        if gold_cls == "no" and pred_cls == "yes":
            gold_diseases = set(self._extract_diseases_ordered(gold))
            pred_diseases = set(self._extract_diseases_ordered(pred))

            # 只有在词表能抽到内容时才应用该规则，避免在无词表或无疾病名时误判
            if gold_diseases and pred_diseases and gold_diseases.isdisjoint(pred_diseases):
                return (
                    0.25,
                    "gold 判定为否；预测未直接回答‘否’，但给出了与标准中疾病不同的诊断 "
                    f"({','.join(sorted(pred_diseases))})，可视为“不是标准中疾病，而是其他疾病”，语义正确"
                )

        # 其余方向不一致的情况，按原逻辑给 0 分
        return 0.0, f"不一致：gold={gold_cls}；pred={pred_cls}"

    def _detect_yes_no(self, text: str) -> tuple[str, bool, list[str]]:
        hits: list[str] = []

        # 1) 严格“是/否”匹配（带边界，避免把“不是”当“是”）
        strict_yes = bool(self.strict_yes_re.search(text))
        strict_no  = bool(self.strict_no_re.search(text))
        if strict_yes:
            hits.append("【严格】是")
        if strict_no:
            hits.append("【严格】否")

        # 2) 先找否定弱同义，再基于它过滤肯定弱同义里的“嵌套误命中”（如 不支持 ⊇ 支持）
        neg_hit = [w for w in self.NEG_WEAK if w in text]

        def _pos_hits_disambiguated(t: str) -> list[str]:
            out = []
            for w in self.POS_WEAK:
                if w not in t:
                    continue
                # 2a) 若任一否定短语本身就包含该肯定词，且该否定短语也在文本里 → 视为被否定包裹，跳过
                if any((w in nw) and (nw in t) for nw in self.NEG_WEAK):
                    continue
                # 2b) 简单负向前缀保护：肯定词前一位若是“ 不 ”则忽略（避免“不支持”“不符合”等）
                #    （更复杂的“未/无/非/不可”等已通过 2a 的短语表兜住了）
                if re.search(r"(?<!不)" + re.escape(w), t):
                    out.append(w)
            return out

        pos_hit = _pos_hits_disambiguated(text)

        if neg_hit:
            hits.append("否定弱同义:" + "|".join(neg_hit))
        if pos_hit:
            hits.append("肯定弱同义:" + "|".join(pos_hit))

        # 3) 归纳 yes/no
        yes = strict_yes or (("是" in text) and ("不是" not in text)) or len(pos_hit) > 0
        no  = strict_no  or len(neg_hit) > 0

        if yes and no:
            return "both", (strict_yes or strict_no), hits
        if yes:
            return "yes", strict_yes, hits
        if no:
            return "no", strict_no, hits
        return "none", False, hits

    def _map_weak_yesno(self, text: str) -> Optional[str]:
        if any(w in text for w in self.NEG_WEAK):
            return "no"
        if any(w in text for w in self.POS_WEAK) or "是" in text:
            return "yes"
        return None

    # ========== choice ========== #
    def _score_choice(self, gold: str, pred: str) -> Tuple[float, str]:
        gold_set, inv_g, gold_raw = self._parse_choices(gold)
        if inv_g:
            return 0.0, f"答案构造包含非法选项：{gold_raw}"

        pred_set, inv_p, pred_raw = self._parse_choices(pred)
        if inv_p:
            return 0.0, f"模型输出包含非法选项：{pred_raw}"
        if not pred_set:
            return 0.0, "未解析到有效选项"

        if pred_set == gold_set:
            return 1.0, "选项完全一致"
        if pred_set.issubset(gold_set) and pred_set:
            lack = ",".join(sorted(gold_set - pred_set))
            return 0.5, f"少选：缺少 {lack}"
        extra = pred_set - gold_set
        if extra:
            return 0.0, f"多选/错选：多余 {','.join(sorted(extra))}"
        return 0.0, "与标准不一致"

    def _parse_choices(self, text: str) -> Tuple[Set[str], bool, str]:
        norm = self._normalize(text)
        norm = self._fw_to_hw(norm).upper()
        letters = self.choice_letter_re.findall(norm)
        raw = "".join(letters)
        invalid = any(ch not in self.CHOICE_ALLOWED for ch in letters)
        return set(letters), invalid, raw

    # ========== compare ========== #
    def _score_compare(self, prompt: str, gold: str, pred: str) -> Tuple[float, str]:
        pair = self._extract_pair_from_prompt(prompt)
        if pair is None:
            return 0.0, "无法从题干抽取两类疾病 A/B"
        A, B, how = pair

        gold_cls, g_detail = self._parse_compare_class(gold, A, B)
        pred_cls, p_detail = self._parse_compare_class(pred, A, B)

        if pred_cls == "ambiguous":
            return 0.0, f"模型输出无法判定（{p_detail}）; A={A}, B={B}, 提取:{how}"
        if gold_cls == "ambiguous":
            return 0.0, f"标准答案无法判定（{g_detail}）; A={A}, B={B}, 提取:{how}"

        if gold_cls == pred_cls:
            return 1.0, f"与标准一致：{gold_cls}；A={A}, B={B}（gold:{g_detail} | pred:{p_detail}）"
        else:
            return 0.0, f"不一致：gold={gold_cls}（{g_detail}）；pred={pred_cls}（{p_detail}）；A={A}, B={B}"

    def _parse_compare_class(self, text: str, A: str, B: str) -> Tuple[str, str]:
        if self._is_normal(text):
            return "both_false", "命中‘正常/未见异常’"

        if any(h in text for h in self.BOTH_TRUE_HINTS_L) and any(h in text for h in self.BOTH_TRUE_HINTS_R):
            return "both_true", "命中‘两者/同时/均/都 + 存在/可见/表现/特征’"

        if any(h in text for h in self.BOTH_FALSE_HINTS_L) and any(h in text for h in self.BOTH_FALSE_HINTS_R):
            return "both_false", "命中‘未见/不支持/均不/都不/无(明显) + 诊断/征象/表现/特征/典型/支持’"
        if f"不支持{A}或{B}" in text or f"不支持{A}和{B}" in text:
            return "both_false", "命中‘不支持A或/和B’"

        has_A = A in text
        has_B = B in text

        if has_A and not has_B:
            return "prefer_A", f"仅命中A:{A}"
        if has_B and not has_A:
            return "prefer_B", f"仅命中B:{B}"

        if has_A and has_B:
            weight_words = ("更", "更接近", "更符合", "可能性更高")
            idx_A = text.rfind(A)
            idx_B = text.rfind(B)
            idx_w = max(text.rfind(w) for w in weight_words)
            if idx_w != -1:
                if abs(idx_w - idx_A) < abs(idx_w - idx_B):
                    return "prefer_A", f"权重词更靠近A:{A}"
                else:
                    return "prefer_B", f"权重词更靠近B:{B}"
            return "ambiguous", "同时出现A/B，但缺少偏向性关键词"

        return "ambiguous", "未出现A/B关键词"

    def _extract_pair_from_prompt(self, prompt: str) -> Optional[Tuple[str, str, str]]:
        cand = self._extract_diseases_ordered(prompt)
        if len(cand) >= 2:
            pair = self._pair_by_connector(prompt, cand)
            if pair:
                return pair[0], pair[1], "lexicon+connector"
            return cand[0], cand[1], "lexicon+fallback"

        for conn in self.CONNECTORS:
            if conn in prompt:
                parts = prompt.split(conn)
                if len(parts) >= 2:
                    left = self._clean_phrase(parts[-2])
                    right = self._clean_phrase(parts[-1])
                    if left and right and left != right:
                        return left, right, "connector-only"
        return None

    def _pair_by_connector(self, text: str, diseases: List[str]) -> Optional[Tuple[str, str]]:
        idxs = []
        for conn in self.CONNECTORS:
            start = 0
            while True:
                k = text.find(conn, start)
                if k == -1:
                    break
                idxs.append(k)
                start = k + 1
        if not idxs:
            return None

        pos = {d: [m.start() for m in re.finditer(re.escape(d), text)] for d in diseases}

        best = None
        best_gap = 10**9
        for ci in idxs:
            left_cand = [(d, max([p for p in pos[d] if p < ci], default=-1)) for d in diseases]
            right_cand = [(d, min([p for p in pos[d] if p > ci], default=10**9)) for d in diseases]
            left_cand = [(d, p) for d, p in left_cand if p >= 0]
            right_cand = [(d, p) for d, p in right_cand if p < 10**8]
            if not left_cand or not right_cand:
                continue
            dL, pL = max(left_cand, key=lambda x: x[1])
            dR, pR = min(right_cand, key=lambda x: x[1])
            gap = pR - pL
            if gap < best_gap and dL != dR:
                best = (dL, dR)
                best_gap = gap
        return best

    # ========== diagnosis ========== #
    def _score_diagnosis(self, gold: str, pred: str) -> Tuple[float, str]:
        gold_norm = self._is_normal(gold)
        pred_norm = self._is_normal(pred)
    
        gold_set = set(self._extract_diseases_ordered(gold))
        pred_set = set(self._extract_diseases_ordered(pred))
    
        # 正常/无异常
        if gold_norm:
            if pred_norm or (not pred_set):
                return 1.0, "标准与预测均为正常/未见异常"
            else:
                return 0.0, f"标准为正常，但预测给出病变：{','.join(sorted(pred_set))}"
    
        if not gold_set:
            return 0.0, "标准答案未解析到任何疾病名"
    
        if pred_norm:
            return 0.0, "预测为正常，但标准包含病变"
    
        if not pred_set:
            return 0.0, "预测未解析到任何疾病名"
    
        # 完全一致
        if pred_set == gold_set:
            return 1.0, f"疾病集合完全一致：{','.join(sorted(gold_set))}"
    
        # 纯少诊（子集）
        if pred_set.issubset(gold_set) and pred_set:
            lack = ",".join(sorted(gold_set - pred_set))
            return 0.5, f"少诊：缺少 {lack}"
    
        # 单病种 vs 单病种 → 更贴切的“错诊”理由
        if len(gold_set) == 1 and len(pred_set) == 1:
            g = next(iter(gold_set))
            p = next(iter(pred_set))
            return 0.0, f"错诊：应为 {g}，预测为 {p}"
    
        # 其它差异：多诊/少诊/混合
        extra = pred_set - gold_set
        lack  = gold_set - pred_set
        if extra and lack:
            return 0.0, f"错诊混合：缺少 {','.join(sorted(lack))}，且多余 {','.join(sorted(extra))}"
        if extra:
            return 0.0, f"多诊：多余 {','.join(sorted(extra))}"
        if lack:
            # 理论上已被子集分支拦截；兜底留着更稳
            return 0.5, f"少诊：缺少 {','.join(sorted(lack))}"
    
        return 0.0, "与标准不一致"

    # ========== 文本/词表工具 ========== #
    def _normalize(self, s: str) -> str:
        if not s:
            return ""
        s = s.strip()
        for k, v in self.PUNCT_MAP.items():
            s = s.replace(k, v)
        s = re.sub(r"\s+", " ", s)
        return s

    def _fw_to_hw(self, s: str) -> str:
        out = []
        for ch in s:
            code = ord(ch)
            if code == 0x3000:
                out.append(" ")
            elif 0xFF01 <= code <= 0xFF5E:
                out.append(chr(code - 0xFEE0))
            else:
                out.append(ch)
        return "".join(out)

    def _is_normal(self, text: str) -> bool:
        return any(w in text for w in self.NORMAL_SYNS)

    def _extract_diseases_ordered(self, text: str) -> List[str]:
        if not self.disease_lexicon:
            return []
        taken = [False] * len(text)
        found: List[Tuple[int, int, str]] = []

        for term in self.disease_lexicon:
            start = 0
            while True:
                k = text.find(term, start)
                if k == -1:
                    break
                span = (k, k + len(term))
                if not any(taken[i] for i in range(span[0], span[1])):  # 无重叠
                    for i in range(span[0], span[1]):
                        taken[i] = True
                    found.append((span[0], span[1], term))
                start = k + 1

        found.sort(key=lambda x: x[0])
        uniq, seen = [], set()
        for _, _, t in found:
            if t not in seen:
                uniq.append(t)
                seen.add(t)
        return uniq

    def _clean_phrase(self, s: str) -> str:
        s = self._normalize(s)
        s = re.sub(r"^(图像更可能属于以下哪种疾病:|根据图像,|患者的眼底情况更符合|图像更接近|更符合|更可能属于|属于)", "", s)
        s = s.strip(" :，,。!?？")
        return s

    def _load_lexicon(self, path: str) -> List[str]:
        try:
            with open(path, "r", encoding="utf-8") as f:
                obj = json.load(f)
            if isinstance(obj, dict):
                for k in ("names", "diseases", "lexicon"):
                    if k in obj and isinstance(obj[k], list):
                        return [str(x) for x in obj[k] if isinstance(x, str)]
                flat = []
                for v in obj.values():
                    if isinstance(v, list):
                        flat.extend([str(x) for x in v if isinstance(x, str)])
                return flat
            elif isinstance(obj, list):
                return [str(x) for x in obj if isinstance(x, str)]
            else:
                raise ValueError("词表 JSON 格式不支持。")
        except Exception as e:
            self._warn(f"载入词表失败：{e}；将不使用词表（compare/diagnosis 仅用启发式）")
            return []

    def _warn(self, msg: str) -> None:
        if self.verbose:
            print(f"[WARN] {msg}", file=sys.stderr)


def main():
    parser = argparse.ArgumentParser(description="中文眼底多任务评测器（含总体正确率与未知题型策略）")
    parser.add_argument("--in_jsonl","-i", required=True, help="输入 JSONL，含 prompt/label/predict/qtype 或 q_type")
    parser.add_argument("--out_jsonl","-o", required=True, help="输出 JSONL，追加 score 与 reason")
    parser.add_argument("--lexicon", default="/home/zhangpinglu/data0/gy/code/fundus-reasoner-dataprocess/configs/all_cn_diease.json", help="可选：疾病词表 all_cn_disease.json")
    parser.add_argument("--metrics_json", default=None, help="可选：总体指标写入路径（默认 <out_jsonl>.metrics.json）")
    parser.add_argument("--unknown_policy", choices=["strict", "skip"], default="skip",
                        help="未知题型处理：strict=报错退出（默认），skip=跳过（在 -v 下告警）")
    parser.add_argument("-v", "--verbose", action="store_true", help="输出调试/告警信息到 stderr（默认关闭）")
    args = parser.parse_args()

    ev = ChineseMedicalEvaluator(disease_lexicon_path=args.lexicon, verbose=args.verbose)
    ev.evaluate_file(args.in_jsonl, args.out_jsonl, args.metrics_json, args.unknown_policy)


if __name__ == "__main__":
    main()
